const htmlFormat = function(str) {
  return style_html(str);
};
export default htmlFormat;

function style_html(html_source, indent_size, indent_character, max_char) {
  //Wrapper function to invoke all the necessary constructors and deal with the output.

  function Parser() {
    this.pos = 0; //Parser position
    this.token = "";
    this.current_mode = "CONTENT"; //reflects the current Parser mode: TAG/CONTENT
    this.tags = {
      //An object to hold tags, their position, and their parent-tags, initiated with default values
      parent: "parent1",
      parentcount: 1,
      parent1: "",
    };
    this.tag_type = "";
    this.token_text = this.last_token = this.last_text = this.token_type = "";

    this.Utils = {
      //Uilities made available to the various functions
      whitespace: "\n\r\t ".split(""),
      single_token: "br,input,link,meta,!doctype,basefont,base,area,hr,wbr,param,img,isindex,?xml,embed".split(","), //all the single tags for HTML
      extra_liners: "head,body,/html".split(","), //for tags that need a line of whitespace before them
      in_array: function(what, arr) {
        for (let i = 0; i < arr.length; i++) {
          if (what === arr[i]) {
            return true;
          }
        }
        return false;
      },
    };

    this.get_content = function() {
      //function to capture regular content between tags

      let char = "";
      const content = [];
      let space = false; //if a space is needed
      while (this.input.charAt(this.pos) !== "<") {
        if (this.pos >= this.input.length) {
          return content.length ? content.join("") : ["", "TK_EOF"];
        }

        char = this.input.charAt(this.pos);
        this.pos++;
        this.line_char_count++;

        if (this.Utils.in_array(char, this.Utils.whitespace)) {
          if (content.length) {
            space = true;
          }
          this.line_char_count--;
          continue; //don't want to insert unnecessary space
        } else if (space) {
          if (this.line_char_count >= this.max_char) {
            //insert a line when the max_char is reached
            content.push("\n");
            for (let i = 0; i < this.indent_level; i++) {
              content.push(this.indent_string);
            }
            this.line_char_count = 0;
          } else {
            content.push(" ");
            this.line_char_count++;
          }
          space = false;
        }
        content.push(char); //letter at-a-time (or string) inserted to an array
      }
      return content.length ? content.join("") : "";
    };

    this.get_script = function() {
      //get the full content of a script to pass to js_beautify

      let char = "";
      const content = [];
      const reg_match = new RegExp("</script" + ">", "igm");
      reg_match.lastIndex = this.pos;
      const reg_array = reg_match.exec(this.input);
      const end_script = reg_array ? reg_array.index : this.input.length; //absolute end of script
      while (this.pos < end_script) {
        //get everything in between the script tags
        if (this.pos >= this.input.length) {
          return content.length ? content.join("") : ["", "TK_EOF"];
        }

        char = this.input.charAt(this.pos);
        this.pos++;

        content.push(char);
      }
      return content.length ? content.join("") : ""; //we might not have any content at all
    };

    this.record_tag = function(tag) {
      //function to record a tag and its parent in this.tags Object
      if (this.tags[tag + "count"]) {
        //check for the existence of this tag type
        this.tags[tag + "count"]++;
        this.tags[tag + this.tags[tag + "count"]] = this.indent_level; //and record the present indent level
      } else {
        //otherwise initialize this tag type
        this.tags[tag + "count"] = 1;
        this.tags[tag + this.tags[tag + "count"]] = this.indent_level; //and record the present indent level
      }
      this.tags[tag + this.tags[tag + "count"] + "parent"] = this.tags.parent; //set the parent (i.e. in the case of a div this.tags.div1parent)
      this.tags.parent = tag + this.tags[tag + "count"]; //and make this the current parent (i.e. in the case of a div 'div1')
    };

    this.retrieve_tag = function(tag) {
      //function to retrieve the opening tag to the corresponding closer
      if (this.tags[tag + "count"]) {
        //if the openener is not in the Object we ignore it
        let temp_parent = this.tags.parent; //check to see if it's a closable tag.
        while (temp_parent) {
          //till we reach '' (the initial value);
          if (tag + this.tags[tag + "count"] === temp_parent) {
            //if this is it use it
            break;
          }
          temp_parent = this.tags[temp_parent + "parent"]; //otherwise keep on climbing up the DOM Tree
        }
        if (temp_parent) {
          //if we caught something
          this.indent_level = this.tags[tag + this.tags[tag + "count"]]; //set the indent_level accordingly
          this.tags.parent = this.tags[temp_parent + "parent"]; //and set the current parent
        }
        delete this.tags[tag + this.tags[tag + "count"] + "parent"]; //delete the closed tags parent reference...
        delete this.tags[tag + this.tags[tag + "count"]]; //...and the tag itself
        if (this.tags[tag + "count"] === 1) {
          delete this.tags[tag + "count"];
        } else {
          this.tags[tag + "count"]--;
        }
      }
    };

    this.get_tag = function() {
      //function to get a full tag and parse its type
      let char = "";
      const content = [];
      let space = false;

      do {
        if (this.pos >= this.input.length) {
          return content.length ? content.join("") : ["", "TK_EOF"];
        }

        char = this.input.charAt(this.pos);
        this.pos++;
        this.line_char_count++;

        if (this.Utils.in_array(char, this.Utils.whitespace)) {
          //don't want to insert unnecessary space
          space = true;
          this.line_char_count--;
          continue;
        }

        if (char === "'" || char === '"') {
          if (!content[1] || content[1] !== "!") {
            //if we're in a comment strings don't get treated specially
            char += this.get_unformatted(char);
            space = true;
          }
        }

        if (char === "=") {
          //no space before =
          space = false;
        }

        if (content.length && content[content.length - 1] !== "=" && char !== ">" && space) {
          //no space after = or before >
          if (this.line_char_count >= this.max_char) {
            this.print_newline(false, content);
            this.line_char_count = 0;
          } else {
            content.push(" ");
            this.line_char_count++;
          }
          space = false;
        }
        content.push(char); //inserts character at-a-time (or string)
      } while (char !== ">");

      const tag_complete = content.join("");
      let tag_index;
      if (tag_complete.indexOf(" ") !== -1) {
        //if there's whitespace, thats where the tag name ends
        tag_index = tag_complete.indexOf(" ");
      } else {
        //otherwise go with the tag ending
        tag_index = tag_complete.indexOf(">");
      }
      const tag_check = tag_complete.substring(1, tag_index).toLowerCase();
      if (tag_complete.charAt(tag_complete.length - 2) === "/" || this.Utils.in_array(tag_check, this.Utils.single_token)) {
        //if this tag name is a single tag type (either in the list or has a closing /)
        this.tag_type = "SINGLE";
      } else if (tag_check === "script") {
        //for later script handling
        this.record_tag(tag_check);
        this.tag_type = "SCRIPT";
      } else if (tag_check === "style") {
        //for future style handling (for now it justs uses get_content)
        this.record_tag(tag_check);
        this.tag_type = "STYLE";
      } else if (tag_check.charAt(0) === "!") {
        //peek for <!-- comment
        if (tag_check.indexOf("[if") !== -1) {
          //peek for <!--[if conditional comment
          if (tag_complete.indexOf("!IE") !== -1) {
            //this type needs a closing --> so...
            const comment = this.get_unformatted("-->", tag_complete); //...delegate to get_unformatted
            content.push(comment);
          }
          this.tag_type = "START";
        } else if (tag_check.indexOf("[endif") !== -1) {
          //peek for <!--[endif end conditional comment
          this.tag_type = "END";
          this.unindent();
        } else if (tag_check.indexOf("[cdata[") !== -1) {
          //if it's a <[cdata[ comment...
          const comment = this.get_unformatted("]]>", tag_complete); //...delegate to get_unformatted function
          content.push(comment);
          this.tag_type = "SINGLE"; //<![CDATA[ comments are treated like single tags
        } else {
          const comment = this.get_unformatted("-->", tag_complete);
          content.push(comment);
          this.tag_type = "SINGLE";
        }
      } else {
        if (tag_check.charAt(0) === "/") {
          //this tag is a double tag so check for tag-ending
          this.retrieve_tag(tag_check.substring(1)); //remove it and all ancestors
          this.tag_type = "END";
        } else {
          //otherwise it's a start-tag
          this.record_tag(tag_check); //push it on the tag stack
          this.tag_type = "START";
        }
        if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) {
          //check if this double needs an extra line
          this.print_newline(true, this.output);
        }
      }
      return content.join(""); //returns fully formatted tag
    };

    this.get_unformatted = function(delimiter, orig_tag) {
      //function to return unformatted content in its entirety

      if (orig_tag && orig_tag.indexOf(delimiter) !== -1) {
        return "";
      }
      let char = "";
      let content = "";
      let space = true;
      do {
        char = this.input.charAt(this.pos);
        this.pos++;

        if (this.Utils.in_array(char, this.Utils.whitespace)) {
          if (!space) {
            this.line_char_count--;
            continue;
          }
          if (char === "\n" || char === "\r") {
            content += "\n";
            for (let i = 0; i < this.indent_level; i++) {
              content += this.indent_string;
            }
            space = false; //...and make sure other indentation is erased
            this.line_char_count = 0;
            continue;
          }
        }
        content += char;
        this.line_char_count++;
        space = true;
      } while (content.indexOf(delimiter) === -1);
      return content;
    };

    this.get_token = function() {
      //initial handler for token-retrieval
      let token;

      if (this.last_token === "TK_TAG_SCRIPT") {
        //check if we need to format javascript
        const temp_token = this.get_script();
        if (typeof temp_token !== "string") {
          return temp_token;
        }
        token = js_beautify(temp_token, this.indent_size, this.indent_character, this.indent_level); //call the JS Beautifier
        return [token, "TK_CONTENT"];
      }
      if (this.current_mode === "CONTENT") {
        token = this.get_content();
        if (typeof token !== "string") {
          return token;
        } else {
          return [token, "TK_CONTENT"];
        }
      }

      if (this.current_mode === "TAG") {
        token = this.get_tag();
        if (typeof token !== "string") {
          return token;
        } else {
          const tag_name_type = "TK_TAG_" + this.tag_type;
          return [token, tag_name_type];
        }
      }
    };

    this.printer = function(js_source, indent_character, indent_size, max_char) {
      //handles input/output and some other printing functions

      this.input = js_source || ""; //gets the input for the Parser
      this.output = [];
      this.indent_character = indent_character || " ";
      this.indent_string = "";
      this.indent_size = indent_size || 2;
      this.indent_level = 0;
      this.max_char = max_char || 70; //maximum amount of characters per line
      this.line_char_count = 0; //count to see if max_char was exceeded

      for (let i = 0; i < this.indent_size; i++) {
        this.indent_string += this.indent_character;
      }

      this.print_newline = function(ignore, arr) {
        this.line_char_count = 0;
        if (!arr || !arr.length) {
          return;
        }
        if (!ignore) {
          //we might want the extra line
          while (this.Utils.in_array(arr[arr.length - 1], this.Utils.whitespace)) {
            arr.pop();
          }
        }
        arr.push("\n");
        for (let i = 0; i < this.indent_level; i++) {
          arr.push(this.indent_string);
        }
      };

      this.print_token = function(text) {
        this.output.push(text);
      };

      this.indent = function() {
        this.indent_level++;
      };

      this.unindent = function() {
        if (this.indent_level > 0) {
          this.indent_level--;
        }
      };
    };
    return this;
  }

  const multi_parser = new Parser(); //wrapping functions Parser
  multi_parser.printer(html_source, indent_character, indent_size); //initialize starting values

  while (true) {
    const t = multi_parser.get_token();
    multi_parser.token_text = t[0];
    multi_parser.token_type = t[1];

    if (multi_parser.token_type === "TK_EOF") {
      break;
    }

    switch (multi_parser.token_type) {
      case "TK_TAG_START":
      case "TK_TAG_SCRIPT":
      case "TK_TAG_STYLE":
        multi_parser.print_newline(false, multi_parser.output);
        multi_parser.print_token(multi_parser.token_text);
        multi_parser.indent();
        multi_parser.current_mode = "CONTENT";
        break;
      case "TK_TAG_END":
        multi_parser.print_newline(true, multi_parser.output);
        multi_parser.print_token(multi_parser.token_text);
        multi_parser.current_mode = "CONTENT";
        break;
      case "TK_TAG_SINGLE":
        multi_parser.print_newline(false, multi_parser.output);
        multi_parser.print_token(multi_parser.token_text);
        multi_parser.current_mode = "CONTENT";
        break;
      case "TK_CONTENT":
        if (multi_parser.token_text !== "") {
          multi_parser.print_newline(false, multi_parser.output);
          multi_parser.print_token(multi_parser.token_text);
        }
        multi_parser.current_mode = "TAG";
        break;
    }
    multi_parser.last_token = multi_parser.token_type;
    multi_parser.last_text = multi_parser.token_text;
  }
  return multi_parser.output.join("");
}
